use std::{ffi::OsStr, path::Path};

use iced::{Element as UiElement, Length, Subscription, Task as UiTask};

use tokio::io::AsyncReadExt as _;

const APPLICATION: &'static str = "com.gitcode.lovejia2022.imshow";

macro_rules! logging {
    () => {
        eprint!("\n")
    };
    ($($arg:tt)*) => {{
        eprint!("{}: ", APPLICATION);
        eprintln!($($arg)*);
    }};
}

fn load_images(path: &Path, images: &mut Vec<String>) -> Result<(), ()> {
    if path.is_file() {
        let ext = path.extension();

        if ext == Some(OsStr::new("png"))
            || ext == Some(OsStr::new("jpg"))
            || ext == Some(OsStr::new("webp"))
            || ext == Some(OsStr::new("jpeg"))
        {
            images.push(path.to_string_lossy().into_owned());
        } else {
            logging!("skip: {path:?}");
        }
        return Ok(());
    }

    if path.is_dir() {
        for ent in std::fs::read_dir(path).map_err(|err| logging!("failed to read dir: {err}"))? {
            let ent = ent.map_err(|err| logging!("failed to read entry: {err}"))?;
            load_images(&ent.path(), images)?;
        }
    }

    Ok(())
}

#[derive(Debug, Clone)]
enum UiMsg {
    Cover,
    Next,
    Prev,
    Quit,
    Delete,
    Seq(Vec<UiMsg>),
    Hook(i32),
    Err,
}

struct UiState {
    images: Vec<ImageInfo>,
    curr: usize,
    image_count: usize,
    cover: bool,
}

#[derive(Clone)]
struct ImageInfo {
    path: String,
    deleted: bool,
}

fn ui_view(state: &UiState) -> UiElement<UiMsg> {
    use iced::widget::text;

    let handle: iced::widget::image::Handle = state.images[state.curr].path.clone().into();

    iced::widget::column![
        iced::widget::image::viewer(handle)
            .width(Length::Fill)
            .height(Length::Fill)
            .content_fit(if state.cover {
                iced::ContentFit::Cover
            } else {
                iced::ContentFit::Contain
            }),
        iced::widget::progress_bar(1.0..=(state.images.len() as f32), (state.curr + 1) as f32),
        text(format!("{}/{}", state.curr + 1, state.images.len(),))
    ]
    .padding(20)
    .spacing(20)
    .into()
}

fn ui_update(state: &mut UiState, message: UiMsg) -> UiTask<UiMsg> {
    logging!("handle message: {message:?}");

    match message {
        UiMsg::Err => {
            return iced::exit();
        }
        UiMsg::Seq(messages) => {
            let mut task = UiTask::none();
            for each in messages {
                task = task.chain(ui_update(state, each));
            }
            return task;
        }
        UiMsg::Cover => state.cover = !state.cover,
        UiMsg::Hook(func) => {
            return UiTask::perform(
                run_hook_script(state.images[state.curr].clone(), func),
                |stdout| {
                    let Ok(stdout) = stdout else {
                        return UiMsg::Err;
                    };

                    let Ok(task) = parse_hook_output(stdout) else {
                        return UiMsg::Err;
                    };

                    task
                },
            )
        }
        UiMsg::Quit => return iced::exit(),
        UiMsg::Next => {
            goto_next_image(state);
        }
        UiMsg::Prev => {
            goto_prev_image(state);
        }
        UiMsg::Delete => {
            delete_current(state);
            goto_next_image(state);
        }
    };

    UiTask::none()
}

fn delete_current(state: &mut UiState) {
    if !state.images[state.curr].deleted {
        state.images[state.curr].deleted = true;
        state.image_count -= 1;
    }
}

fn goto_next_image(state: &mut UiState) {
    let n = state.images.len();
    let mut i = state.curr + 1;

    while state.images[i % n].deleted {
        i += 1;
    }

    state.curr = i % n;
}

fn goto_prev_image(state: &mut UiState) {
    let n = state.images.len();
    let mut i = state.curr + n - 1;

    while state.images[i % n].deleted {
        i = i + n - 1;
    }

    state.curr = i % n;
}

fn parse_hook_output(output: String) -> Result<UiMsg, ()> {
    let mut tasks = vec![];

    for line in output.split('\n') {
        let line = line.trim();

        if line.is_empty() {
            continue;
        }

        let Some(cmd) = line.strip_prefix("imshow: ") else {
            logging!("invalid hook output: {line:?}");
            return Err(());
        };

        tasks.push(match cmd {
            "next" => UiMsg::Next,
            "prev" => UiMsg::Prev,
            "delete" => UiMsg::Delete,
            _ => {
                logging!("undefined command: {cmd}");
                return Err(());
            }
        });
    }

    Ok(UiMsg::Seq(tasks))
}

fn ui_subscription(_: &UiState) -> Subscription<UiMsg> {
    iced::keyboard::on_key_release(|key, modifiers| {
        Some(match key {
            iced::keyboard::Key::Character(name) => match on_key(name.as_str(), modifiers) {
                Some(some) => some,
                None => return None,
            },
            _ => return None,
        })
    })
}

fn on_key(c: &str, modifiers: iced::keyboard::Modifiers) -> Option<UiMsg> {
    if !modifiers.is_empty() {
        return None;
    }

    Some(match c {
        "c" => UiMsg::Cover,
        "p" => UiMsg::Prev,
        "n" => UiMsg::Next,
        "q" => UiMsg::Quit,
        "d" => UiMsg::Delete,

        "0" => UiMsg::Hook(0),
        "1" => UiMsg::Hook(1),
        "2" => UiMsg::Hook(2),
        "3" => UiMsg::Hook(3),
        "4" => UiMsg::Hook(4),
        "5" => UiMsg::Hook(5),
        "6" => UiMsg::Hook(6),
        "7" => UiMsg::Hook(7),
        "8" => UiMsg::Hook(8),
        "9" => UiMsg::Hook(9),

        _ => {
            logging!("unhandled key: {c}");
            return None;
        }
    })
}

fn try_main() -> Result<(), ()> {
    let mut images = vec![];

    for path in std::env::args().skip(1) {
        load_images(path.as_ref(), &mut images)?;
    }

    if images.is_empty() {
        println!("nothing to show");
        return Ok(());
    }

    iced::application(APPLICATION, ui_update, ui_view)
        .subscription(ui_subscription)
        .centered()
        .antialiasing(true)
        .run_with(move || {
            (
                UiState {
                    image_count: images.len(),
                    curr: 0,
                    cover: false,
                    images: images
                        .into_iter()
                        .map(|path| ImageInfo {
                            path,
                            deleted: false,
                        })
                        .collect(),
                },
                UiTask::none(),
            )
        })
        .map_err(|err| logging!("iced error: {err}"))?;

    Ok(())
}

async fn run_hook_script(image_info: ImageInfo, func: i32) -> Result<String, ()> {
    let program = Path::new("./imshow_hook");

    if !program.is_file() {
        logging!("ignore hook because can't find hook script imshow_hook");
        return Err(());
    }

    let Ok(mut proc) = tokio::process::Command::new(program)
        .env("IMSHOW_HOOK_IMAGE", image_info.path)
        .env(
            "IMSHOW_HOOK_IMAGE_DELETED",
            if image_info.deleted { "YES" } else { "NO" },
        )
        .env("IMSHOW_HOOK_FUNC", func.to_string())
        .stdout(std::process::Stdio::piped())
        .spawn()
    else {
        logging!("failed to start imshow hook");
        return Err(());
    };

    let stdout = proc.stdout.take();

    let stdout = match proc.wait().await {
        Ok(exit) => {
            if !exit.success() {
                logging!("hook program report error: {:?}", exit.code());
                return Err(());
            }

            let mut buf = String::new();

            if let Err(err) = stdout.unwrap().read_to_string(&mut buf).await {
                logging!("failed to read hook stdout: {err}");
                return Err(());
            }

            buf
        }
        Err(err) => {
            logging!("failed to read hook stdout: {err}");
            return Err(());
        }
    };

    Ok(stdout)
}

fn main() {
    if try_main().is_err() {
        std::process::exit(1)
    }
}
